package com.katesoft.scale4j.web.webconsole;

import com.katesoft.scale4j.common.utils.StringUtility;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.log.LogFactory;
import it.openutils.log4j.Log4jConfigurationServlet;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.DefaultHandler;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mortbay.jetty.security.Constraint;
import org.mortbay.jetty.security.ConstraintMapping;
import org.mortbay.jetty.security.SecurityHandler;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.thread.QueuedThreadPool;
import org.perf4j.log4j.GraphingStatisticsAppender;
import org.perf4j.log4j.servlet.GraphingServlet;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import static com.katesoft.scale4j.common.io.FileUtility.LINE_SEPARATOR;
import static com.katesoft.scale4j.common.utils.StringUtility.isEmpty;
import static com.katesoft.scale4j.persistent.utils.Perf4jLoggerUtility.findGraphingStatisticsAppender;

/**
 * This is jetty server with some additional functionality:
 * <p/>
 * 1)log4j configuration console available by default on /log4j context path.
 * <p/>
 * 2)perf4j statistic viewer available by default on /perf4j context path.
 * <p/>
 * 3)hazelcast monitor GWT web application available by default on /hazelcast context path
 * <p/>
 * Client can use this server as standard jetty server - additional functionality of this class will not override user's handler, but will add functionality.
 * You can customize context path for log4j, perf4j, and hazelcast by using setters for those properties.
 *
 * @author kate2007
 */
public class WebMonitor extends Server implements InitializingBean
{
    private Logger logger = LogFactory.getLogger(getClass());
    //
    private String contextPath = "/";
    private String perf4jPath = "/perf4j";
    private String log4jPath = "/log4j";
    private String hazelcastPath = "/hazelcast";
    //
    private String graphNames;
    private String hazelcastWarLocation;
    private AuthenticationManager authenticationManager;

    public WebMonitor()
    {
        setThreadPool(new QueuedThreadPool(1000));
    }

    /**
     * allows to set context path of web monitoring console.
     *
     * @param contextPath path
     */
    public void setContextPath(String contextPath)
    {
        if (!isEmpty(contextPath)) { this.contextPath = contextPath; }
    }

    /**
     * allows to set context path for perf4j servlet.
     *
     * @param perf4jPath path
     */
    public void setPerf4jPath(String perf4jPath)
    {
        if (!isEmpty(perf4jPath)) { this.perf4jPath = perf4jPath; }
    }

    /**
     * allows to set context path for hazelcast servlet.
     *
     * @param hazelcastPath path
     */
    public void setHazelcastPath(String hazelcastPath)
    {
        if (!isEmpty(hazelcastPath)) { this.hazelcastPath = hazelcastPath; }
    }

    /**
     * allows to set context path for log4j configuration servlet.
     *
     * @param log4jPath path
     */
    public void setLog4jPath(String log4jPath)
    {
        if (!isEmpty(log4jPath)) {this.log4jPath = log4jPath;}
    }

    /** @return context path for perf4j servlet(default is /perf4j). */
    public String getPerf4jPath()
    {
        return perf4jPath;
    }

    /** @return context path of log4f servlet(default is /log4j). */
    public String getLog4jPath()
    {
        return log4jPath;
    }

    /** @return context path of hazelcast monitor(default is /hazelcast). */
    public String getHazelcastPath()
    {
        return hazelcastPath;
    }

    /**
     * The graphNames parameter determines which graphs to expose.
     * <p/>
     * The param-value should be a comma-separated list of the appender NAMES as defined in the log4j.xml file.
     * <p/>
     * NOTE: you can leave this property as null and do not inject list - in this case graphNames will be auto-detected.
     *
     * @param graphNames comma-separated list of the appender names.
     */
    public void setGraphNames(String graphNames)
    {
        this.graphNames = graphNames;
    }

    /**
     * you can manually specify location of hazelcast-monitor.war
     * @param hazelcastWarLocation actual location of war
     */
    public void setHazelcastWarLocation(String hazelcastWarLocation)
    {
        this.hazelcastWarLocation = hazelcastWarLocation;
    }

    /**
     * create web application for hazelcast web-monitor.
     * <p/>
     * Take a look here <a href="http://hazelcast.com/documentation.jsp#Monitoring">monitor</a>
     *
     * @return webAppContext for hazelcast war file
     */
    protected WebAppContext setupHazelcast()
    {
        if (hazelcastWarLocation != null) {
            WebAppContext webapp = new WebAppContext();
            webapp.setContextPath(hazelcastPath);
            webapp.setWar(hazelcastWarLocation);
            return webapp;
        }
        return null;
    }

    /**
     * set-up log4j configuration servlet
     *
     * @param context servlet context(log4j servlet should be registered with-in context).
     */
    protected void setupLog4jConfigurationServlet(Context context)
    {
        ServletHolder holder = new ServletHolder();
        Log4jConfigurationServlet servlet = new Log4jConfigurationServlet();
        holder.setServlet(servlet);
        holder.setName("log4j");
        logger.info("registering log4j servlet holder[%s]", getLog4jPath());
        context.addServlet(holder, getLog4jPath());
    }

    /**
     * set-up perf4j configuration servlet.
     *
     * @param context servlet context(perf4j servlet should be registered with-in context).
     */
    protected void setupPerf4jServlet(Context context)
    {
        Map<String, String> initParameters = Collections.singletonMap("graphNames", graphNames);
        ServletHolder holder = new ServletHolder();
        GraphingServlet servlet = new GraphingServlet();
        holder.setServlet(servlet);
        holder.setName("perf4j");
        holder.setInitParameters(initParameters);
        logger.info("registering perf4j servlet holder[%s]", getPerf4jPath());
        context.addServlet(holder, getPerf4jPath());
    }

    /**
     * allows to specify authenticationManager to be used to restrict access to webconsole.
     *
     * @param authenticationManager manager
     */
    public void setAuthenticationManager(AuthenticationManager authenticationManager)
    {
        this.authenticationManager = authenticationManager;
    }

    /** @return true if hazelcast-monitor web application available in classpath or if user specified location of war manually. */
    public boolean isHazelcastAvailable()
    {
        return !isEmpty(hazelcastWarLocation);
    }

    protected SecurityHandler getSecurityRestrictionHandler()
    {
        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__BASIC_AUTH);
        constraint.setAuthenticate(true);
        constraint.setRoles(new String[]{Constraint.ANY_ROLE});
        constraint.setAuthenticate(true);
        ConstraintMapping cm = new ConstraintMapping();
        cm.setConstraint(constraint);
        cm.setPathSpec("/*");
        SecurityHandler sh = new SecurityHandler();
        sh.setConstraintMappings(new ConstraintMapping[]{cm});
        sh.setUserRealm(new JettySpringSecurityUserRealm(authenticationManager));
        sh.setCheckWelcomeFiles(false);
        return sh;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (StringUtility.isEmpty(graphNames)) {
            Collection<GraphingStatisticsAppender> statisticsAppender = findGraphingStatisticsAppender();
            StringBuilder builder = new StringBuilder();
            for (Iterator<GraphingStatisticsAppender> iterator = statisticsAppender.iterator(); iterator.hasNext();) {
                GraphingStatisticsAppender appender = iterator.next();
                builder.append(appender.getName());
                if (iterator.hasNext()) {
                    builder.append(",");
                }
                graphNames = builder.toString();
            }
        }
        logger.info("runtime performance graphNames = [%s]", graphNames);
        //
        if (StringUtility.isEmpty(hazelcastWarLocation)) {
            Properties properties = new Properties();
            properties.load(new ClassPathResource("hazelcast-runtime.properties").getInputStream());
            String hazelcast = String.format("hazelcast-monitor-%s.war", properties.getProperty("hazelcast.version"));
            String classpath = System.getProperty("java.class.path");
            logger.info("looking %s in classpath%s[%s]", hazelcast, LINE_SEPARATOR, classpath);
            String[] arr = classpath.split(";");
            for (String s : arr) {
                if (s.contains(hazelcast)) {
                    hazelcastWarLocation = s;
                    break;
                }
            }
        }
        if (hazelcastWarLocation != null) {
            logger.info("using hazelcast-monitor web archive on location=%s", hazelcastWarLocation);
        }
        else {
            logger.warn("unable to find hazelcast-monitor in classpath!");
        }
        //
        Context servletContext = new Context();
        WebAppContext webAppContext = setupHazelcast();
        servletContext.setContextPath(contextPath);
        setupPerf4jServlet(servletContext);
        setupLog4jConfigurationServlet(servletContext);
        //
        HandlerCollection handlerCollection = new HandlerCollection();
        Handler userHandler = getHandler();
        if (userHandler != null) {
            handlerCollection.addHandler(userHandler);
        }
        if (webAppContext != null) {
            handlerCollection.addHandler(webAppContext);
        }
        handlerCollection.addHandler(servletContext);
        if (authenticationManager != null) {
            SecurityHandler securityHandler = getSecurityRestrictionHandler();
            if (webAppContext != null) { webAppContext.addHandler(securityHandler); }
        }
        if (userHandler == null) {
            handlerCollection.addHandler(new DefaultHandler());
        }
        setHandler(handlerCollection);
        //
        start();
    }
}
